Skip to content

[中文版本](/posts/c教程/zh-CN/07-贴近系统/070-Practice Mini Shell in C)

Follow-Along Deliverable ​

  • Assumed state: lessons 061-069 are complete and you can rebuild the previous example.
  • Working directory: ~/c-course-labs/070-mini-shell.
  • First command: on macOS / Linux run mkdir -p ~/c-course-labs/070-mini-shell && cd ~/c-course-labs/070-mini-shell; on Windows PowerShell run New-Item -ItemType Directory -Force "$HOME\c-course-labs\070-mini-shell"; Set-Location "$HOME\c-course-labs\070-mini-shell".
  • Success evidence: keep the source file, executable, evidence.md, and record pwd, echo, and one pipe or redirection command record.
  • Boundary for this lab: This lab builds an observable shell prototype. Job control, a full scripting language, and a production sandbox are out of scope.
  • Reset: remove the executable, temporary data, and generated output for this lab; keep source and evidence.md for review.

It’s time to bring together everything you’ve learned so far, system calls, process creation, pipes, redirection, and signal handling, into one cohesive project.

In this section, you’ll build a minimal interactive shell, just likebash orzsh, but stripped down to the essentials. It will run commands, handle input/output redirection, and even support pipelines.

Step 1. What You’ll Build ​

Your mini shell will:

  1. Display a prompt like$
  2. Read user input (e.g.,ls -l,cat file.txt)
  3. Parse it into command and arguments
  4. Create a new process usingfork()
  5. Replace the process image with the requested command usingexecvp()
  6. Wait for the child to finish

Optional extensions:

  • Handle signals (Ctrl+C) gracefully
  • Redirect output to a file (> redirection)
  • Chain commands using pipes (|)

Step 2. Core Loop Skeleton ​

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX 1024
int main(void) {
    char input[MAX];
    while (1) {
        printf("$ ");
        fflush(stdout);
        if (!fgets(input, sizeof(input), stdin))
            break;
        // Remove newline
        input[strcspn(input, "\n")] = 0;
        // Exit command
        if (strcmp(input, "exit") == 0)
            break;
        // Tokenize input
        char *args[64];
        int i = 0;
        char *token = strtok(input, " ");
        while (token) {
            args[i++] = token;
            token = strtok(NULL, " ");
        }
        args[i] = NULL;
        // Fork and execute
        pid_t pid = fork();
        if (pid == 0) {
            execvp(args[0], args);
            perror("execvp");
            exit(1);
        } else if (pid > 0) {
            wait(NULL);
        } else {
            perror("fork");
        }
    }
    printf("Goodbye!\n");
    return 0;
}

Compile and run:

gcc mini_shell.c -o mini_shell
./mini_shell

Try commands:

$ ls
$ pwd
$ echo hello world
$ exit

Step 3. Handling Errors Gracefully ​

If you enter a wrong command:

$ xyz

Output:

execvp: No such file or directory

This happens because the program handlesexecvp() failure properly withperror(), just as you learned in section 69.

Step 4. Adding Signal Handling ​

Let’s make Ctrl+C stop the running command, but not kill the shell itself.

#include <signal.h>
void sigint_handler(int sig) {
    printf("\nType 'exit' to quit.\n$ ");
    fflush(stdout);
}
int main(void) {
    signal(SIGINT, sigint_handler);
    ...
}

Now the shell ignores Ctrl+C while waiting for input, instead of terminating.

Step 5. Supporting Output Redirection ​

We’ll add> redirection like:

$ echo Hello > out.txt

Add this before theexecvp() call in the child:

#include <fcntl.h>
for (int j = 0; args[j]; j++) {
    if (strcmp(args[j], ">") == 0) {
        args[j] = NULL;
        int fd = open(args[j + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
        dup2(fd, STDOUT_FILENO);
        close(fd);
        break;
    }
}

Nowstdout of the command goes to the file instead of the screen.

Step 6. Supporting Input Redirection ​

Similarly, for< redirection:

$ cat < in.txt

Add:

if (strcmp(args[j], "<") == 0) {
    args[j] = NULL;
    int fd = open(args[j + 1], O_RDONLY);
    dup2(fd, STDIN_FILENO);
    close(fd);
    break;
}

Step 7. Adding Pipe Support ​

To handle commands like:

$ ls | wc -l

We create two processes connected by a pipe.

int pipefd[2];
pipe(pipefd);
pid_t p1 = fork();
if (p1 == 0) {
    dup2(pipefd[1], STDOUT_FILENO);
    close(pipefd[0]);
    execlp("ls", "ls", NULL);
}
pid_t p2 = fork();
if (p2 == 0) {
    dup2(pipefd[0], STDIN_FILENO);
    close(pipefd[1]);
    execlp("wc", "wc", "-l", NULL);
}
close(pipefd[0]);
close(pipefd[1]);
wait(NULL);
wait(NULL);

That’s the same pattern you saw in section 64,ls | wc -l.

Step 8. Combining All Features ​

Your shell now:

  • Parses user input
  • Spawns child processes
  • Handles I/O redirection
  • Supports Ctrl+C interruption
  • Runs simple pipelines

With ~150 lines of code, you have a working Unix shell prototype.

Step 9. Tiny Code: Full Mini Shell ​

Here’s the clean, minimal version:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
void sigint_handler(int sig) {
    printf("\n$ ");
    fflush(stdout);
}
int main(void) {
    signal(SIGINT, sigint_handler);
    char input[1024];
    while (1) {
        printf("$ ");
        fflush(stdout);
        if (!fgets(input, sizeof(input), stdin)) break;
        input[strcspn(input, "\n")] = 0;
        if (strcmp(input, "exit") == 0) break;
        char *args[64];
        int i = 0;
        char *token = strtok(input, " ");
        while (token) {
            args[i++] = token;
            token = strtok(NULL, " ");
        }
        args[i] = NULL;
        pid_t pid = fork();
        if (pid == 0) {
            for (int j = 0; args[j]; j++) {
                if (strcmp(args[j], ">") == 0) {
                    int fd = open(args[j + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
                    dup2(fd, STDOUT_FILENO);
                    close(fd);
                    args[j] = NULL;
                } else if (strcmp(args[j], "<") == 0) {
                    int fd = open(args[j + 1], O_RDONLY);
                    dup2(fd, STDIN_FILENO);
                    close(fd);
                    args[j] = NULL;
                }
            }
            execvp(args[0], args);
            perror("execvp");
            exit(1);
        } else if (pid > 0) {
            wait(NULL);
        }
    }
    printf("Exiting shell.\n");
    return 0;
}

Try it out, it’s a real, interactive shell.

Step 10. Why It Matters ​

This exercise combines everything you’ve learned in Chapter 7:

  • System calls (fork,exec,wait,pipe,dup2)
  • Signals (SIGINT)
  • File descriptors and redirection
  • Error handling witherrno
  • Environment inheritance

You’ve just built a simplified version of the core that powers every Unix shell, frombash tozsh tofish.

Try It Yourself ​

  1. Add support for pipelines (|) by chaining multiple commands.
  2. Implement background processes with&.
  3. Addcd andpwd as built-in commands.
  4. Display the exit code after each command.
  5. Handle multiple spaces and quoted arguments.

Next, we’ll move into Chapter 8: Debugging, Testing, and Profiling, starting with gdb, your most powerful ally in understanding and fixing C programs.

本站总访问 — 次